状态库 unstated-next
是什么
基于 React 的 createContext(创建上下文) + useContext(使用上下文)实现的
怎么做
使用方法:只需要将状态存储在 StoreContext 中,Provider 下的任意子组件都可以通过 useContext 获取到上下文中的状态。
提供 createContainer 将自定义 Hook 封装为一个数据对象,提供"Provider"注入与"useContainer"获取 Store 这两个方法。 "Provider":就是对"value"进行了约束,固化了 Hooks 返回的 value 直接作为 value 传递给"Context.Provider"这个规范 "useContainer":就是对"React.useContext(Context)"的封装
原理
- 一个创建容器的方法:"createContainer"
- "createContainer"该方法接收一个自定义 hooks 作为参数,先使用原生 React.createContext 创建 Context 对象;
- 存在一个"Provider"方法,在其内部执行传递给 createContainer 的 Hooks 得到一个 value 值,返回内部 Context.Provider 的封装。
- 存在一个"useContainer"方法,在其内部使用"useContext"获取当前 Context 的值,并返回。
- 一个使用容器的方法:"useContainer"
- "useContainer"该方法内部其实就是返回我们创建的 Container 实例所对应的值。
源码
import React from "react"
const EMPTY: unique symbol = Symbol()
/* 类型 */
interface ContainerProviderProps<State = void> {
initialState?: State
children: React.ReactNode
}
interface Container<Value, State = void> {
Provider: React.ComponentType<ContainerProviderProps<State>>
useContainer: () => Value
}
// 1. 创建内容管理者
export function createContainer<Value, State = void>(
useHook: (initialState?: State) => Value,
): Container<Value, State> {
let Context = React.createContext<Value | typeof EMPTY>(EMPTY)
function Provider(props: ContainerProviderProps<State>) {
let value = useHook(props.initialState)
return <Context.Provider value={value}>{props.children}</Context.Provider>
}
// 内容
function useContainer(): Value {
let value = React.useContext(Context)
if (value === EMPTY) {
throw new Error("Component must be wrapped with <Container.Provider>")
}
return value
}
return { Provider, useContainer }
}
// 内容
function useContainer<Value, State = void>(
container: Container<Value, State>,
): Value {
return container.useContainer()
}
Example
store.js
import { useState } from 'react';
import { createContainer } from 'unstated-next';
const useListStore = (
initialState = {
count:0
},
) => {
const [listStates, setListStates] = useState(initialState);
const changeListStates = newListStates => {
setListStates(prevStates => Object.assign({}, prevStates, newListStates));
};
return { listStates, changeListStates };
};
export const ListStore = createContainer(useListStore);
index.js
import { ListStore } from './store.js';
const Layout = ({ content }) => {
return (
<ListStore.Provider>
<Latest />
</ListStore.Provider>
);
};
export default Layout;
Latest.js
import { ListStore } from '../store';
const Latest = () => {
const listStore = ListStore.useContainer();
const { listStates, changeListStates } = listStore;
const { count } = listStates;
const useCounter = () => {
changeListStates({
count: count + 1,
});
}
return (
<div className={styles.wrap}>
<span>{count}</span>
<button onClick={useCounter}>+</button>
</div>
);
};
export default Latest;
解决了什么问题
在不使用 unstated-next 库时,我们针对函数式组件,想使用 Context 的话必然会在子组件中显式的使用"useContext"和"React.createContext.Provider"。
因此,unstated-next 要做的就是针对"Provider 作固定封装"及针对"useContext”api 进行隐藏不显式提供使用而封装的一个库,全代码就 40 行。
缺点
当不同子组件引用 Container 里面的不同的数据时,当更新某个属性时,另一个未使用到组件也会被更新,这个原因是 useContainer 提供的数据流是一个引用整体,部分子节点的更新会引起整个 Hook 重新执行,因而所有引用它组件也会 re-render,不同实现"按需更新"